{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "f725852e-71ef-4615-8cac-011a516fbe72",
   "metadata": {},
   "source": [
    "# Agent Executor From Scratch\n",
    "\n",
    "In this notebook we will go over how to build a basic agent executor from\n",
    "scratch.\n",
    "\n",
    "![diagram](./img/agent-executor-diagram.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0860511-03c2-49bb-937b-035f84142b7e",
   "metadata": {},
   "source": [
    "## Setup\n",
    "\n",
    "First we need to install the packages required\n",
    "\n",
    "```bash\n",
    "yarn add @langchain/community @langchain/anthropic @langchain/langgraph @langchain/core\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f4179ce-48fa-4aaf-a5a1-027b5229be1a",
   "metadata": {},
   "source": [
    "Next, we need to set API keys for Anthropic (the LLM we will use) and Tavily (the\n",
    "search tool we will use)\n",
    "\n",
    "```bash\n",
    "export ANTHROPIC_API_KEY=\n",
    "export TAVILY_API_KEY=\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37943b1c-2b0a-4c09-bfbd-5dc24b839e3c",
   "metadata": {},
   "source": [
    "Optionally, we can set API key for\n",
    "[LangSmith tracing](https://smith.langchain.com/), which will give us\n",
    "best-in-class observability.\n",
    "\n",
    "```bash\n",
    "export LANGCHAIN_TRACING_V2=true\n",
    "export LANGCHAIN_API_KEY=\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "972e58b3-fe3c-449d-b3c4-8fa2217afd07",
   "metadata": {},
   "source": [
    "## Define the graph schema\n",
    "\n",
    "We first need to define the graph state. The state for a traditional LangGraph agent has a single attribute, `messages` which holds the original input, as well as the conversation history."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4",
   "metadata": {},
   "outputs": [],
   "source": [
    "import { Annotation } from \"@langchain/langgraph\";\n",
    "import { BaseMessage } from \"@langchain/core/messages\";\n",
    "\n",
    "const AgentState = Annotation.Root({\n",
    "  messages: Annotation<BaseMessage[]>({\n",
    "    reducer: (x, y) => x.concat(y),\n",
    "  }),\n",
    "});"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec3fab3b",
   "metadata": {},
   "source": [
    "## Define tools\n",
    "\n",
    "Next, we will define the tools we'll use in this LangGraph graph. For this example, we'll use the pre-build Tavily Search tool, however you can use any pre-built, or custom tool you'd like. See [this guide](https://js.langchain.com/docs/how_to/custom_tools/) on how to create custom LangChain tools.\n",
    "\n",
    "We'll want to pass our tools to the `ToolNode`, which is the way LangGraph executes tools."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "25789946",
   "metadata": {},
   "outputs": [],
   "source": [
    "import { TavilySearch } from \"@langchain/tavily\";\n",
    "import { ToolNode } from \"@langchain/langgraph/prebuilt\";\n",
    "\n",
    "const tools = [new TavilySearch({ maxResults: 1 })];\n",
    "\n",
    "const toolNode = new ToolNode<typeof AgentState.State>(tools);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd27b281-cc9a-49c9-be78-8b98a7d905c4",
   "metadata": {},
   "source": [
    "## Define the nodes\n",
    "\n",
    "We now need to define a few different nodes in our graph. In `langgraph`, a node\n",
    "can be either a function or a\n",
    "[runnable](https://js.langchain.com/docs/expression_language/). There are two\n",
    "main nodes we need for this:\n",
    "\n",
    "1. The agent: responsible for deciding what (if any) actions to take.\n",
    "2. A function to invoke tools: if the agent decides to take an action, this node\n",
    "   will then execute that action.\n",
    "\n",
    "We will also need to define some edges. Some of these edges may be conditional.\n",
    "The reason they are conditional is that based on the output of a node, one of\n",
    "several paths may be taken. The path that is taken is not known until that node\n",
    "is run (the LLM decides).\n",
    "\n",
    "1. Conditional Edge: after the agent is called, we should either: a. If the\n",
    "   agent said to take an action, then the function to invoke tools should be\n",
    "   called b. If the agent said that it was finished, then it should finish\n",
    "2. Normal Edge: after the tools are invoked, it should always go back to the\n",
    "   agent to decide what to do next\n",
    "\n",
    "Let's define the nodes, as well as a function to decide how what conditional\n",
    "edge to take."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "d61a970d-edf4-4eef-9678-28bab7c72331",
   "metadata": {},
   "outputs": [],
   "source": [
    "import type { RunnableConfig } from \"@langchain/core/runnables\";\n",
    "import { ChatAnthropic } from \"@langchain/anthropic\";\n",
    "import { END } from \"@langchain/langgraph\";\n",
    "\n",
    "// Define the LLM to be used in the agent\n",
    "const llm = new ChatAnthropic({\n",
    "  model: \"claude-3-5-sonnet-20240620\",\n",
    "  temperature: 0,\n",
    "}).bindTools(tools); // Ensure you bind the same tools passed to the ToolExecutor to the LLM, so these tools can be used in the agent\n",
    "\n",
    "// Define logic that will be used to determine which conditional edge to go down\n",
    "const shouldContinue = (data: typeof AgentState.State): \"executeTools\" | typeof END => {\n",
    "  const { messages } = data;\n",
    "  const lastMsg = messages[messages.length - 1];\n",
    "  // If the agent called a tool, we should continue. If not, we can end.\n",
    "  if (!(\"tool_calls\" in lastMsg) || !Array.isArray(lastMsg.tool_calls) || !lastMsg?.tool_calls?.length) {\n",
    "    return END;\n",
    "  }\n",
    "  // By returning the name of the next node we want to go to\n",
    "  // LangGraph will automatically route to that node\n",
    "  return \"executeTools\";\n",
    "};\n",
    "\n",
    "const callModel = async (data: typeof AgentState.State, config?: RunnableConfig): Promise<Partial<typeof AgentState.State>> => {\n",
    "  const { messages } = data;\n",
    "  const result = await llm.invoke(messages, config);\n",
    "  return {\n",
    "    messages: [result],\n",
    "  };\n",
    "};\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0b211f4-0c5c-4792-b18d-cd70907c71e7",
   "metadata": {},
   "source": [
    "## Define the graph\n",
    "\n",
    "We can now put it all together and define the graph!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0",
   "metadata": {},
   "outputs": [],
   "source": [
    "import { START, StateGraph } from \"@langchain/langgraph\";\n",
    "\n",
    "// Define a new graph\n",
    "const workflow = new StateGraph(AgentState)\n",
    "  // Define the two nodes we will cycle between\n",
    "  .addNode(\"callModel\", callModel)\n",
    "  .addNode(\"executeTools\", toolNode)\n",
    "  // Set the entrypoint as `callModel`\n",
    "  // This means that this node is the first one called\n",
    "  .addEdge(START, \"callModel\")\n",
    "  // We now add a conditional edge\n",
    "  .addConditionalEdges(\n",
    "    // First, we define the start node. We use `callModel`.\n",
    "    // This means these are the edges taken after the `agent` node is called.\n",
    "    \"callModel\",\n",
    "    // Next, we pass in the function that will determine which node is called next.\n",
    "    shouldContinue,\n",
    "  )\n",
    "  // We now add a normal edge from `tools` to `agent`.\n",
    "  // This means that after `tools` is called, `agent` node is called next.\n",
    "  .addEdge(\"executeTools\", \"callModel\");\n",
    "\n",
    "const app = workflow.compile();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "214ae46e-c297-465d-86db-2b0312ed3530",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(node) callModel:  AIMessage {\n",
      "  \"id\": \"msg_01SwpMu2zu8W4NZeLEg5DHKj\",\n",
      "  \"content\": [\n",
      "    {\n",
      "      \"type\": \"text\",\n",
      "      \"text\": \"Certainly! I'll use the Tavily search engine to find current weather information for San Francisco (SF). Let me search for that information for you.\"\n",
      "    },\n",
      "    {\n",
      "      \"type\": \"tool_use\",\n",
      "      \"id\": \"toolu_013Asn4HooNPMtYYapQPcewB\",\n",
      "      \"name\": \"tavily_search_results_json\",\n",
      "      \"input\": {\n",
      "        \"input\": \"current weather in San Francisco\"\n",
      "      }\n",
      "    }\n",
      "  ],\n",
      "  \"additional_kwargs\": {\n",
      "    \"id\": \"msg_01SwpMu2zu8W4NZeLEg5DHKj\",\n",
      "    \"type\": \"message\",\n",
      "    \"role\": \"assistant\",\n",
      "    \"model\": \"claude-3-5-sonnet-20240620\",\n",
      "    \"stop_reason\": \"tool_use\",\n",
      "    \"stop_sequence\": null,\n",
      "    \"usage\": {\n",
      "      \"input_tokens\": 416,\n",
      "      \"output_tokens\": 94\n",
      "    }\n",
      "  },\n",
      "  \"response_metadata\": {\n",
      "    \"id\": \"msg_01SwpMu2zu8W4NZeLEg5DHKj\",\n",
      "    \"model\": \"claude-3-5-sonnet-20240620\",\n",
      "    \"stop_reason\": \"tool_use\",\n",
      "    \"stop_sequence\": null,\n",
      "    \"usage\": {\n",
      "      \"input_tokens\": 416,\n",
      "      \"output_tokens\": 94\n",
      "    },\n",
      "    \"type\": \"message\",\n",
      "    \"role\": \"assistant\"\n",
      "  },\n",
      "  \"tool_calls\": [\n",
      "    {\n",
      "      \"name\": \"tavily_search_results_json\",\n",
      "      \"args\": {\n",
      "        \"input\": \"current weather in San Francisco\"\n",
      "      },\n",
      "      \"id\": \"toolu_013Asn4HooNPMtYYapQPcewB\",\n",
      "      \"type\": \"tool_call\"\n",
      "    }\n",
      "  ],\n",
      "  \"invalid_tool_calls\": [],\n",
      "  \"usage_metadata\": {\n",
      "    \"input_tokens\": 416,\n",
      "    \"output_tokens\": 94,\n",
      "    \"total_tokens\": 510\n",
      "  }\n",
      "}\n",
      "----\n",
      "\n",
      "(node) executeTools:  ToolMessage {\n",
      "  \"content\": \"[{\\\"title\\\":\\\"Weather in San Francisco\\\",\\\"url\\\":\\\"https://www.weatherapi.com/\\\",\\\"content\\\":\\\"{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724101106, 'localtime': '2024-08-19 13:58'}, 'current': {'last_updated_epoch': 1724100300, 'last_updated': '2024-08-19 13:45', 'temp_c': 21.1, 'temp_f': 70.0, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 9.4, 'wind_kph': 15.1, 'wind_degree': 250, 'wind_dir': 'WSW', 'pressure_mb': 1019.0, 'pressure_in': 30.08, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 64, 'cloud': 0, 'feelslike_c': 21.1, 'feelslike_f': 70.0, 'windchill_c': 17.7, 'windchill_f': 63.8, 'heatindex_c': 17.7, 'heatindex_f': 63.8, 'dewpoint_c': 10.7, 'dewpoint_f': 51.2, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 13.2, 'gust_kph': 21.2}}\\\",\\\"score\\\":0.9953496,\\\"raw_content\\\":null}]\",\n",
      "  \"name\": \"tavily_search_results_json\",\n",
      "  \"additional_kwargs\": {},\n",
      "  \"response_metadata\": {},\n",
      "  \"tool_call_id\": \"toolu_013Asn4HooNPMtYYapQPcewB\"\n",
      "}\n",
      "----\n",
      "\n",
      "(node) callModel:  AIMessage {\n",
      "  \"id\": \"msg_01PwNrBEDix98RpZSdWwzjjn\",\n",
      "  \"content\": \"Based on the search results, I can provide you with the current weather information for San Francisco (SF). Here's a summary of the weather conditions:\\n\\n1. Temperature: 21.1°C (70.0°F)\\n2. Condition: Sunny\\n3. Wind: 15.1 km/h (9.4 mph), direction WSW (West-Southwest)\\n4. Humidity: 64%\\n5. Precipitation: 0 mm (0 inches)\\n6. Cloud cover: 0% (clear sky)\\n7. Visibility: 16 km (9 miles)\\n8. UV Index: 5.0 (moderate)\\n\\nThe weather in San Francisco is currently pleasant and sunny. It's a comfortable 21.1°C (70.0°F) with no cloud cover, making it a clear and bright day. The wind is light to moderate, coming from the west-southwest direction. There's no precipitation, and visibility is good.\\n\\nIt's worth noting that San Francisco's weather can change quickly due to its unique microclimate, so if you're planning activities, it's always good to check for updates closer to the time.\\n\\nIs there anything specific about the weather in San Francisco that you'd like to know more about?\",\n",
      "  \"additional_kwargs\": {\n",
      "    \"id\": \"msg_01PwNrBEDix98RpZSdWwzjjn\",\n",
      "    \"type\": \"message\",\n",
      "    \"role\": \"assistant\",\n",
      "    \"model\": \"claude-3-5-sonnet-20240620\",\n",
      "    \"stop_reason\": \"end_turn\",\n",
      "    \"stop_sequence\": null,\n",
      "    \"usage\": {\n",
      "      \"input_tokens\": 1028,\n",
      "      \"output_tokens\": 282\n",
      "    }\n",
      "  },\n",
      "  \"response_metadata\": {\n",
      "    \"id\": \"msg_01PwNrBEDix98RpZSdWwzjjn\",\n",
      "    \"model\": \"claude-3-5-sonnet-20240620\",\n",
      "    \"stop_reason\": \"end_turn\",\n",
      "    \"stop_sequence\": null,\n",
      "    \"usage\": {\n",
      "      \"input_tokens\": 1028,\n",
      "      \"output_tokens\": 282\n",
      "    },\n",
      "    \"type\": \"message\",\n",
      "    \"role\": \"assistant\"\n",
      "  },\n",
      "  \"tool_calls\": [],\n",
      "  \"invalid_tool_calls\": [],\n",
      "  \"usage_metadata\": {\n",
      "    \"input_tokens\": 1028,\n",
      "    \"output_tokens\": 282,\n",
      "    \"total_tokens\": 1310\n",
      "  }\n",
      "}\n",
      "----\n",
      "\n",
      "Final Result:  Based on the search results, I can provide you with the current weather information for San Francisco (SF). Here's a summary of the weather conditions:\n",
      "\n",
      "1. Temperature: 21.1°C (70.0°F)\n",
      "2. Condition: Sunny\n",
      "3. Wind: 15.1 km/h (9.4 mph), direction WSW (West-Southwest)\n",
      "4. Humidity: 64%\n",
      "5. Precipitation: 0 mm (0 inches)\n",
      "6. Cloud cover: 0% (clear sky)\n",
      "7. Visibility: 16 km (9 miles)\n",
      "8. UV Index: 5.0 (moderate)\n",
      "\n",
      "The weather in San Francisco is currently pleasant and sunny. It's a comfortable 21.1°C (70.0°F) with no cloud cover, making it a clear and bright day. The wind is light to moderate, coming from the west-southwest direction. There's no precipitation, and visibility is good.\n",
      "\n",
      "It's worth noting that San Francisco's weather can change quickly due to its unique microclimate, so if you're planning activities, it's always good to check for updates closer to the time.\n",
      "\n",
      "Is there anything specific about the weather in San Francisco that you'd like to know more about?\n"
     ]
    }
   ],
   "source": [
    "import { HumanMessage, BaseMessage } from \"@langchain/core/messages\";\n",
    "\n",
    "let finalResult: BaseMessage | undefined;\n",
    "\n",
    "const prettyLogOutput = (output: Record<string, any>) => {\n",
    "  const keys = Object.keys(output);\n",
    "  const firstItem = output[keys[0]];\n",
    "  if (\"messages\" in firstItem) {\n",
    "    console.log(`(node) ${keys[0]}:`, firstItem.messages[0]);\n",
    "    console.log(\"----\\n\");\n",
    "  }\n",
    "}\n",
    "\n",
    "const inputs = { messages: [new HumanMessage(\"Search the web for the weather in sf\")] };\n",
    "for await (const s of await app.stream(inputs)) {\n",
    "  prettyLogOutput(s);\n",
    "  if (\"callModel\" in s && s.callModel.messages?.length) {\n",
    "    finalResult = s.callModel.messages[0];\n",
    "  }\n",
    "}\n",
    "\n",
    "console.log(\"Final Result: \", finalResult.content)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "TypeScript",
   "language": "typescript",
   "name": "tslab"
  },
  "language_info": {
   "codemirror_mode": {
    "mode": "typescript",
    "name": "javascript",
    "typescript": true
   },
   "file_extension": ".ts",
   "mimetype": "text/typescript",
   "name": "typescript",
   "version": "3.7.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
